Deep Understanding of Fold in Haskell
fold is a very important combinator in functional programming. In Haskell four versions of fold operation
are provided: foldl, foldr, foldl' and foldr'. Now lets take a close look at them. We’ll gain a better
understanding of the underlying principles behind fold operation and learn how to use them correctly and
efficiently in our real world applications.
Implementation
fold is a higher order function that takes a function and a list as arguments, look at the
detailed implementations of these four fold functions in the
base library.
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f z0 xs0 = lgo z0 xs0
where
lgo z [] = z
lgo z (x:xs) = lgo (f z x) xs
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr k z = go
where
go [] = z
go (y:ys) = y `k` go ys
foldl' :: (b -> a -> b) -> b -> [a] -> b
foldl' f z0 xs0 = lgo z0 xs0
where lgo z [] = z
lgo z (x:xs) = let z' = f z x in z' `seq` lgo z' xs
foldr' :: (a -> b -> b) -> b -> t a -> b
foldr' f z0 xs = foldl f' id xs z0
where f' k x z = k $! f x z
foldl means left-associative fold operation, folding up a list form the left to right and foldr are
right-associative fold operation. foldl' and foldr' are strict version of foldl and foldr.
We can see that foldl and foldl' are both tail recursion and foldr and foldr' are not.
Because Haskell is a programming language with non-strict evaluation strategy, the lazy version of foldl
and foldr will accumulate too many unevaluated chunks on heap when the list to fold over is too long.
There’s also a picture from Wikipedia that can illustrate the core feature of foldl and foldr very well:

Another significant difference is that foldr runs forwards while foldl runs backwards. The function
foldl (flip (:)) [] can reverse a list while foldr (:) [] returns a list unchanged. This is why
foldr can fold over infinite lists while foldl can’t.
Benchmark
I make a basic benchmark to measure the performance of these four version of fold operation, and the
result of benchmark is corresponded with our knowledge about above detailed implementations of fold
operation. The task is computation summary of an integer sequence [1..1000000].
r = @fold (+) 0 ([1..1000000] :: [Int])
All experiments are done using GHC 8.0.1 with -O2 optimization on 64bit Windows 10 (CPU: Intel 5600U).
CPU Usage
I use the criterion which is a well-known framework for executing and analyzing benchmark in Haskell to perform CPU time usage benchmark.
Case CPU time
foldl 174.8 ms
foldr 29.23 ms
foldl' 9.806 ms
foldr' 87.38 ms
foldl' is the fastest version of these four fold operations. I have also noticed an interesting phenomenon that
foldr with -O optimization is four times slower than using -O2 optimization option.
Memory Allocation
Fpcomplete has developed a very convenient tool called weigh which can measure allocations in Haskell program. The result is as follows:
Case Bytes GCs Check
foldl 169,259,440 322 OK
foldr 96,646,080 153 OK
foldl' 95,999,920 186 OK
foldr' 127,999,920 247 OK
foldl' use the least memory.
Why foldl is in the first place ?
Why foldl, rather than foldl', be placed in the first place, although foldl performs
so poor ? A possible reason is, when Haskell 1.0 was published there’s no seq function
in language standard, there was foldl no choice but to define foldl in a such way. The
seq was introduced in Haskell 1.3. foldl was not changed and mainstream Haskell compiler
added the foldl' function.
Best Practice
After analysis the principles of fold operation, we can conclude some best practical strategies to improve
performance when use fold operator in Haskell:
- When we are sure that the list to fold is finite, the fold computation will terminate, then
foldl'would be the best choice. foldris the only fold function that can process infinite lists, when the binary function has the property of short-circuit evaluation (like logical and&&and logical or||), the computation will terminate. Be careful thatfoldr'can’t be used to process infinite list.- Under almost all conditions the
foldlcombinator has the worse performance both in time usage and memory allocations and shouldn’t be used in production-level code, as the Haskell Wiki says:foldlis rarely the right choice.